package fge;

import com.mxgraph.model.mxCell;
import com.mxgraph.model.mxGeometry;
import com.mxgraph.model.mxICell;
import com.mxgraph.util.mxConstants;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * A component has a function, some ports and can have subcomponents.
 */
public class Fcomponent extends Fvertex implements doubleClickable
{
	private String func = "";
	private CodeEditor editor;

	private static final String color_blue = "#c3d9ff";
	private static final String color_green = "#9ce79c";
	private static final String color_yellow = "#ffffc3";
	private static final String color_red = "#ffc3c3";
	
	public static final String color_SDF = color_blue;
	public static final String color_SY = color_green;
	public static final String color_DE = color_yellow;
	public static final String color_CT = color_red;

	public Fcomponent(String label, double width, double height)
	{
		this(label, 0, 0, width, height);
	}

	public Fcomponent(String label, double x, double y, double width, double height)
	{
		super();
		
		// JGraph specifics
		mxGeometry geo = new mxGeometry(x, y, width, height);
		this.setGeometry(geo);
		this.changeStyle(mxConstants.STYLE_ROUNDED, 1);
		this.changeStyle(mxConstants.STYLE_WHITE_SPACE, "wrap");
		this.changeStyle(mxConstants.STYLE_OPACITY, 50);
		this.setId(null);			// Just assign an incremental id's
		this.setVertex(true);		// This is a vertex, not an edge
		this.setConnectable(false);	// Don't allow in or out-going edges; use Ports instead

		this.setNum_inputs(1);
		this.setNum_outputs(1);
		this.setName(label);
	}

	/**
	 * We normally think of a component as being hierarchical if it has subcomponents.
	 * However, we shall define a hierarchical component as a component that has internal wiring.
	 * Thus if a component has no sub-components but still has internal wiring, 
	 * it is a hierarchical component. 
	 * Since internal wiring is nessecary for connecting subcomponents, this definition is sufficient.
	 * By internal wiring is meant that the user has drawn signals between child Fvertex'es.
	 * @return True if hierarchical, False if atomic.
	 */
	public boolean isHierarchical() {
		return ((this.getChildEdges().size() > 0) || (this.getChildComponents().size() > 0));
	} 

	public boolean isAtomic() {
		return !this.isHierarchical();
	} 

	protected String HaskellInputPostFunction(InputPort p) {
		if (p instanceof InputPortSY) {
			InputPortSY p2 = (InputPortSY)p;
			return p2.getUniqueName();
		}
		if (p instanceof InputPortSDF) {
			InputPortSDF p2 = (InputPortSDF)p;
			return String.format("(%d,%s)", p2.getToken_rate(), p2.getUniqueName());
		}
		if (p instanceof InputPortDE) {
			InputPortDE p2 = (InputPortDE)p;
			if (p2.getInitial_value().isEmpty()) {
				// No initial value given
				return p2.getUniqueName();
			} else {
				return String.format("(%s,%s)", p2.getInitial_value(), p2.getUniqueName());
			}
		}
		if (p instanceof InputPortCT) {
			InputPortCT p2 = (InputPortCT)p;
			return String.format("(%s,%s)", p2.getInitial_value(), p2.getUniqueName());
		}
		
		return p.getUniqueName();
	}

	protected String Haskell_function_outro() {
		boolean first_iteration = true;
		String forsyde_function_outro = "";
		for (InputPort p : this.getInputPorts()) {
			String op = HaskellMoCLookup(p) + (first_iteration ? ".§>" : ".>§>");
			forsyde_function_outro += op + " " + HaskellInputPostFunction(p) + " ";
			first_iteration = false;
		}
		
		String post = "";
		if (this instanceof SYprocess)	{
			String delay = ((SYprocess)this).getDelay();
			if (!delay.isEmpty())
				post = "SY.>§ " + delay;
		}
		if (this instanceof SDFprocess)	{post = "SDF.>§ undefined";} 
		if (this instanceof DEprocess)	{post = "DE.>§ " + ((DEprocess)this).getDelay();}
		if (this instanceof CTprocess)	{post = "CT.>§ " + ((CTprocess)this).getDelay();}
		
		forsyde_function_outro += post;
		return forsyde_function_outro;
	}
	
	protected String HaskellAtomicDefaultFunction() {
		String ret = "";
		String outro = "";
		ret += this.getHaskellDefinition();

		boolean include_outro = true;
		if (this instanceof Fchannel) include_outro = false;
		if (this.getOutputPorts().size() >= 2) include_outro = false;
		if (this.getInputPorts().size() == 0) include_outro = false;
		if (include_outro)
			outro = this.Haskell_function_outro();
		
		ret += String.format("%s %s = <function> %s%n", 
				this.getUniqueName(), 
				arguments(this.getInputPorts()), 
				outro
		);
		
		return ret;
	}
	
	
	public void openEditor() {
		if (this.isHierarchical()) {
			return;	// Don't allow editing a function if it will be generated automatically
		}
		
		/**
		 * If process has no function, give it some boiler-plate function.
		 */
		if (this.getFunc().isEmpty()) {
			this.setFunc(this.HaskellAtomicDefaultFunction());
		}
		
		if (this.editor == null) {
			// No editor window open, make one (it opens automatically)
			this.editor = new CodeEditor(this);	// Open function code editor
		} else {
			// Editor window already open, just give it focus
			this.editor.requestFocus();
			this.editor.toFront();
		}
	}

	/**
	 * The editor window itself calls this method just before closing.
	 * It signifies that no editor window is now currently open.
	 */
	public void removeEditor() {
		this.editor = null;
	}
	
	public List<InputPort> getInputPorts() {
		List<InputPort> iports = new ArrayList<InputPort>();
		if (this.children != null) {
			for (Object obj : this.children) {
				if (obj instanceof InputPort) {
					iports.add((InputPort)obj);
				}
			}
		}
		return iports;
	}

	public List<OutputPort> getOutputPorts() {
		List<OutputPort> oports = new ArrayList<OutputPort>();
		if (this.children != null) {
			for (Object obj : this.children) {
				if (obj instanceof OutputPort) {
					oports.add((OutputPort)obj);
				}
			}
		}
		return oports;
	}

	/**
	 * Distribute children evenly across the height of their parent
	 */
	private void alignPorts() {
		int cnt;

		cnt = 1;
		List<InputPort> iports = this.getInputPorts();
		double dist = 1.0 / (iports.size() + 1);
		for (InputPort it : iports) {
			mxGeometry geom = (mxGeometry)it.getGeometry().clone();
			double y = dist * cnt++;
			geom.setY(y);
			it.setGeometry(geom);
		}

		cnt = 1;
		List<OutputPort> oports = this.getOutputPorts();
		dist = 1.0 / (oports.size() + 1);
		for (OutputPort it : oports) {
			mxGeometry geom = (mxGeometry)it.getGeometry().clone();
			double y = dist * cnt++;
			geom.setY(y);
			it.setGeometry(geom);
		}
	}

	protected void addInput() {
		InputPort p = new InputPort();
		this.insert(p);
	}

	private void removeInput() {
		List<InputPort> ports = this.getInputPorts();
		if (ports.size() >= 1) {
			mxICell p = ports.get(ports.size()-1);
			this.remove(p);		// remove last port
		}
	}

	private void addOutput() {
		OutputPort p = new OutputPort();
		this.insert(p);
	}

	private void removeOutput() {
		List<OutputPort> ports = this.getOutputPorts();
		if (ports.size() >= 1) {
			mxICell p = ports.get(ports.size()-1);
			this.remove(p);		// remove last port
		}
	}

	public List<IOport> getIOports() {
		List<IOport> ret = new ArrayList<IOport>();
		
		if (this.children == null) {
			return ret;	// JGraph will first allocate space for children, when an mxCell has some
		}
		for (Object o : this.children) {
			if (o instanceof IOport)
				ret.add((IOport)o);
		}
		
		return ret;
	}
	
	public List<Fcomponent> getChildComponents() {
		List<Fcomponent> ret = new ArrayList<Fcomponent>();
		
		if (this.children == null) {
			return ret;	// JGraph will first allocate space for children, when an mxCell has some
		}
		for (Object o : this.children) {
			if (o instanceof Fcomponent)
				ret.add((Fcomponent)o);
		}
		
		return ret;
	}
	
	/**
	 * Take the 1 incoming edge on p, and return the source port of this edge.
	 * Situation: src -----edge-----> p; we return src.
	 * Note that src-port can be either an InputPort or an OutputPort:
	 *	src is an InputPort if the Fcomponent only has wiring,
	 *	src in an OutputPort if the Fcomponent has sub-Fcomponents.
	 * @param p
	 * @return The source-port of the edge driving p
	 */
	private IOport getDrivingSource(IOport p) {
		List<mxCell> incoming = p.getIncomingEdges();
		
		if (incoming.size() != 1) {
			throw new RuntimeException("Output port " + p.getUniqueName() + " must have exactly 1 incoming edge");
		}
		
		mxICell psrc = incoming.get(0).getSource();
		if (!(psrc instanceof IOport)) {
			throw new RuntimeException("Edge going out of non-IOport");
		}

		return (IOport)psrc;
	}
	
	private List<IOport> getDrivingSources(List<? extends IOport> ports) {
		List<IOport> ret = new ArrayList<IOport>();
		for (IOport o : ports) {
			ret.add(getDrivingSource(o));
		}
		return ret;
	}
	
	private String tuplify(List<? extends IOport> ports) {
		List<String> names = new ArrayList<String>();
		for (IOport p : ports) {
			names.add(p.getUniqueName());
		}
		return tuplify_strings(names);
	}

	private String tuplify_strings(List<String> names) {
		String ret = "";
		for (Iterator<String> it = names.iterator(); it.hasNext();) {
			ret += it.next() + (it.hasNext() ? "," : "");
		}
		
		/**
		 * Only make a tuple if more than one port.
		 */
		if (names.size() > 1) {
			ret = String.format("(%s)", ret);
		}
		return ret;
	}
	
	private String arguments(List<? extends IOport> iports) {
		String ret = "";
		List<String> names = new ArrayList<String>();

		for (IOport p : iports) {
			names.add(p.getUniqueName());
		}

		for (Iterator<String> it = names.iterator(); it.hasNext();) {
			ret += it.next() + (it.hasNext() ? " " : "");
		}
		
		return ret;
	}	
	
	protected String getHaskellCode_Hierarchical() {
		final String indent = "\t";
		final String newline = System.getProperty("line.separator");
		String ret = "";
		
		String inputs = tuplify(this.getInputPorts());
		String outputs = tuplify(getDrivingSources(this.getOutputPorts()));

		/**
		 * First line of function body:	model inputs = outputs
		 */
		ret = String.format("%s %s = %s%n", this.getUniqueName(), inputs, outputs);

		if (this.getChildComponents().isEmpty()) {
			/**
			 * This hierarchical component contains no subcomponents.
			 * However since it is hierarchical, it must contain some internal 
			 * wiring (directly from input to output), but this is handled 
			 * already in the above inputs and outpus.
			 */
			return ret;
		}

		ret += indent + "where" + newline;
		
		for (Fcomponent subcomponent : this.getChildComponents()) {
			ret += indent;
			ret += String.format("%s = %s %s%n", 
					tuplify(subcomponent.getOutputPorts()), 
					subcomponent.getUniqueName(),
					arguments(getDrivingSources(subcomponent.getInputPorts()))
			);
		}
		
		return ret;
	}

	protected String getHaskellCode_Atomic() {
		if (this.getFunc().isEmpty()) {
			throw new RuntimeException("Atomic process " + this.getUniqueName() + " must not have empty function!");
		}
		
		return this.getFunc();	// Return user-written code if process is atomic
	}

	protected String HaskellMoCLookup(IOport port) {
		String ret = "";
		mxICell port_parent = port.getParent();

		if (port_parent instanceof SDFprocess)	{ret += "SDF";} else 
		if (port_parent instanceof SYprocess)	{ret += "SY";} else 
		if (port_parent instanceof DEprocess)	{ret += "DE";} else 
		if (port_parent instanceof CTprocess)	{ret += "CT";} else 
		if (port_parent instanceof Fchannel)	{
			if (((Fchannel)port_parent).getInputPorts().contains(port)) {
				return ((Fchannel)port_parent).getFrom_MoC();
			} else if (((Fchannel)port_parent).getOutputPorts().contains(port)) {
				return ((Fchannel)port_parent).getTo_MoC();
			} else {
				throw new RuntimeException("Port is neither input nor output");
			}
		}
		return ret;
	}
	
	protected String getHaskellType(IOport port) {
		return (HaskellMoCLookup(port) + ".Signal " + port.getType());
	}
	
	protected String getHaskellDefinition() {
		String ret = "";
		String input = "";
		String output = "";
		List<String> outputs = new ArrayList<String>();
		
		for (InputPort p : this.getInputPorts()) {
			input += getHaskellType(p);
			input += " -> ";
		}

		for (OutputPort p : this.getOutputPorts()) {
			outputs.add(getHaskellType(p));
		}
		output = tuplify_strings(outputs);
		
		ret += String.format("%s :: %s%s%n", this.getUniqueName(), input, output);
		
		return ret;
	}
	
	public String getHaskellCode() {
		String ret = "";
		if (this.isHierarchical()) {
			ret += getHaskellDefinition();	// Hierarchical's must get their definition automatically
			ret += this.getHaskellCode_Hierarchical();
		} else {
			ret += this.getHaskellCode_Atomic();
		}
		return ret;
	}

	
	/*
	 * Getter and setters below are mostly used by BeanInfo
	 */


	/**
	 * @return The current actual Haskell/SystemC code
	 */
	public String getFunc() {
		return func;
	}

	/**
	 * @param func Set the actual Haskell/SystemC code to this
	 */
	public void setFunc(String func) {
		this.func = func;
	}

	/**
	 * The BeanInfo tells the property editor to call this when it needs to display the value.
	 * The value we display is a dummy string, an we only use Function as a decorator 
	 * for Fcomponent in which .toString is overriden to return the dummy string.
	 * @return Decorator used for purposes of opening the real CodeEditor
	 */
	public Function getFunction() {
		return new Function(this);
	}

	/**
	 * When the property editor thinks the user has updated the value, this 
	 * method is called to inform us of this new value. However, since we only 
	 * wanted to open the CodeEditor, the Fcomponent::setFunc() has already been set in 
	 * FuncPropertyField::setValue(); so do nothing.
	 * @param func Decorator which is ignored
	 */
	public void setFunction(Function func) {
		// Do nothing
	}	
	
	public int getNum_inputs() {
		return this.getInputPorts().size();
	}

	public final void setNum_inputs(int count) {
		count = java.lang.Math.max(count, 0);	// Can not have negative input ports, but 0 is allowed
		List<InputPort> ports = this.getInputPorts();

		if (count == ports.size()) {
			return; // No change
		}

		int diff = count - ports.size();
		if (diff > 0) {
			// Add diff-number of ports
			for (int i = 0; i < diff; i++) {
				this.addInput();
			}
		} else {
			diff = -diff;
			// Remove diff-number of ports
			for (int i = 0; i < diff; i++) {
				this.removeInput();
			}
		}
		this.alignPorts();
	}

	public int getNum_outputs() {
		return this.getOutputPorts().size();
	}

	public final void setNum_outputs(int count) {
		count = java.lang.Math.max(count, 1);	// Must have at least 1 output port
		List<OutputPort> ports = this.getOutputPorts();

		if (count == ports.size()) {
			return; // No change
		}

		int diff = count - ports.size();
		if (diff > 0) {
			// Add diff-number of ports
			for (int i = 0; i < diff; i++) {
				this.addOutput();
			}
		} else {
			diff = -diff;
			// Remove diff-number of ports
			for (int i = 0; i < diff; i++) {
				this.removeOutput();
			}
		}
		this.alignPorts();
	}
}
